/// <reference path="libraries/jquery.js" />

/// 
/// CRM Soap Service Facade
///
/// Based on the 
///         XrmServiceToolkit - A Microsoft Dynamics CRM 2011 & CRM 2013 JavaScript Library https://xrmservicetoolkit.codeplex.com/
///         CrmFetchKit.js - Script Library for Dynamics CRM (SOAP) https://crmfetchkit.codeplex.com/
///         
///         Many thanks to these two projects and all their contributors!
///
/// authors
///     tuneCRM (tunecrm@gmail.com)
/// 
/// version
///     1.0.1 (beta)
///
/// requires 
///     jquery.1.8.2.js
///
(function ($, jQuery) {
  window.CrmSoapFacade = function () {

    ///
    /// Private member
    ///
    var soapEndpoint = '/XRMServices/2011/Organization.svc/web',
      // cache for the context
      globalContext = null,
      // cache for the server-url
      serverUrl = null,
      // empty guid
      emptyGuid = "00000000-0000-0000-0000-000000000000";
    
    ///
    /// Private function to the context object.
    ///

    function getContext() {

      if (globalContext === null) {

        if (typeof window.GetGlobalContext !== "undefined") {

          /*ignore jslint start*/
          globalContext = window.GetGlobalContext();
          /*ignore jslint end*/
        } else {

          if (typeof Xrm !== "undefined") {
            globalContext = Xrm.Page.context;
          } else if (typeof window.parent.Xrm != "undefined") {
            globalContext = window.parent.Xrm.Page.context;
          } else {
            throw new Error("Context is not available.");
          }
        }
      }

      return globalContext;
    }

    ///
    /// Private function to return the server URL from the context
    ///

    function getServerUrl() {

      if (serverUrl === null) {

        var url = null,
          localserverUrl = window.location.protocol + "//" + window.location.host,
          context = getContext();

        if (context.getClientUrl !== undefined) {
          url = context.getClientUrl();
        } else if (context.isOutlookClient() && !context.isOutlookOnline()) {
          url = localserverUrl;
        } else {
          url = context.getServerUrl();
          url = url.replace(/^(http|https):\/\/([_a-zA-Z0-9\-\.]+)(:([0-9]{1,5}))?/, localserverUrl);
          url = url.replace(/\/$/, "");
        }

        serverUrl = url;

      }

      return serverUrl;
    }

    function executeRequest(xml, requestType, async) {

      async = (async === false) ? false : true;

      return $.ajax({
        type: 'POST',
        async: async,
        data: xml,
        url: getServerUrl() + soapEndpoint,
        headers: {
          "Accept": "application/xml, text/xml, */*",
          "Content-Type": "text/xml; charset=utf-8",
          "SOAPAction": ("http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/" + requestType)
        }
      });
    }

    function doRequest(soapBody, requestType, async) {

      var dfd = $.Deferred();

      var request =
      ["<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>",
        "<soap:Body>",
        "<", requestType, " xmlns='http://schemas.microsoft.com/xrm/2011/Contracts/Services' xmlns:i='http://www.w3.org/2001/XMLSchema-instance'>", soapBody, "</", requestType, ">",
        "</soap:Body>",
        "</soap:Envelope>"
      ].join("");

      executeRequest(request, requestType, async).then(function (data, status, xhr) {

        dfd.resolve(data, xhr);

      }, function (jqXhr, textStatus, errorThrown) {
        var errMessage = getResponseXmlErrorMessage(jqXhr);
        if (errMessage != null) {
          textStatus = 'errorRecognized';
          errorThrown = errMessage;
        }

        var error = { response: jqXhr, status: textStatus, message: errorThrown };
        dfd.reject(error);
      });

      return dfd.promise();
    }

    function doRequestWithSimpleResponse(request, requestType, async) {
      var dfd = $.Deferred();
      doRequest(request, requestType, async).then(function (data, xhr) {
        var response = $(data).find((requestType + 'Result')).eq(0);
        dfd.resolve(response);
      },
        dfd.reject);

      return dfd.promise();
    }

    var createEntity = function (entity, async) {
      var request = entity.serialize();
      var dfd = $.Deferred();
      doRequestWithSimpleResponse(request, "Create", async).then(function (response) {
        var result = crmXmlDecode(response.text());
        dfd.resolve(result);
      },
        dfd.reject);

      return dfd.promise();
    };

    var updateEntity = function (entity, async) {
      var request = entity.serialize();
      return doRequestWithSimpleResponse(request, "Update", async);
    };

    var deleteEntity = function (entityName, id, async) {
      var request = [
        "<entityName>", entityName, "</entityName>",
        "<id>", id, "</id>"
      ].join("");

      return doRequestWithSimpleResponse(request, "Delete", async);
    };

    var execute = function (request, async) {
      return doRequestWithSimpleResponse(request, "Execute", async);
    };

    var retrieveEntity = function (entityName, id, columnSet, async) {
      var attributes = "";
      var query = "";
      if (columnSet != null) {
        for (var i = 0; i < columnSet.length; i++) {
          attributes += "<b:string>" + columnSet[i] + "</b:string>";
        }
        query = "<a:AllColumns>false</a:AllColumns>" +
          "<a:Columns xmlns:b='http://schemas.microsoft.com/2003/10/Serialization/Arrays'>" +
          attributes +
          "</a:Columns>";
      } else {
        query = "<a:AllColumns>true</a:AllColumns><a:Columns xmlns:b='http://schemas.microsoft.com/2003/10/Serialization/Arrays' />";
      }

      var msgBody =
      [
        "<entityName>", entityName, "</entityName>",
        "<id>", id, "</id>",
        "<columnSet xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts'>",
        query,
        "</columnSet>"
      ].join("");

      var dfd = $.Deferred();

      doRequest(msgBody, "Retrieve", async).then(function (data) {
        var response = $(data).find("RetrieveResult").eq(0)[0];
        var entity = parseSingleEntityNode(response);
        dfd.resolve(entity);
      }, function (error) {
        if (error.status === 'errorRecognized') {
          if (error.message.indexOf('Does Not Exist') != -1) {
            dfd.resolve(null);
          } else {
            dfd.reject(error);
          }
        }
      });

      return dfd.promise();
    };

    ///
    /// Assigns the record (target) to system-user or team (assignee)
    /// (target and assignee should be either 'crmEntity' or 'crmEntityReference')
    ///

    var assignEntity = function (target, assignee, async) {
      var request = "<request i:type='b:AssignRequest' xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts' xmlns:b='http://schemas.microsoft.com/crm/2011/Contracts'>" +
        "<a:Parameters xmlns:c='http://schemas.datacontract.org/2004/07/System.Collections.Generic'>" +
        "<a:KeyValuePairOfstringanyType>" +
        "<c:key>Target</c:key>" +
        "<c:value i:type='a:EntityReference'>" +
        "<a:Id>" + target.id + "</a:Id>" +
        "<a:LogicalName>" + target.logicalName + "</a:LogicalName>" +
        "<a:Name i:nil='true' />" +
        "</c:value>" +
        "</a:KeyValuePairOfstringanyType>" +
        "<a:KeyValuePairOfstringanyType>" +
        "<c:key>Assignee</c:key>" +
        "<c:value i:type='a:EntityReference'>" +
        "<a:Id>" + assignee.id + "</a:Id>" +
        "<a:LogicalName>" + assignee.logicalName + "</a:LogicalName>" +
        "<a:Name i:nil='true' />" +
        "</c:value>" +
        "</a:KeyValuePairOfstringanyType>" +
        "</a:Parameters>" +
        "<a:RequestId i:nil='true' />" +
        "<a:RequestName>Assign</a:RequestName>" +
        "</request>";

      return execute(request, async);
    };

    ///
    /// Changes state/status for the record (moniker)
    /// (moniker should be either 'crmEntity' or 'crmEntityReference')
    ///

    var setState = function (moniker, stateCode, statusCode, async) {
      var request = [
        "<request i:type='b:SetStateRequest' xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts' xmlns:b='http://schemas.microsoft.com/crm/2011/Contracts'>",
        "<a:Parameters xmlns:c='http://schemas.datacontract.org/2004/07/System.Collections.Generic'>",
        "<a:KeyValuePairOfstringanyType>",
        "<c:key>EntityMoniker</c:key>",
        "<c:value i:type='a:EntityReference'>",
        "<a:Id>", moniker.id, "</a:Id>",
        "<a:LogicalName>", moniker.logicalName, "</a:LogicalName>",
        "<a:Name i:nil='true' />",
        "</c:value>",
        "</a:KeyValuePairOfstringanyType>",
        "<a:KeyValuePairOfstringanyType>",
        "<c:key>State</c:key>",
        "<c:value i:type='a:OptionSetValue'>",
        "<a:Value>", stateCode.toString(), "</a:Value>",
        "</c:value>",
        "</a:KeyValuePairOfstringanyType>",
        "<a:KeyValuePairOfstringanyType>",
        "<c:key>Status</c:key>",
        "<c:value i:type='a:OptionSetValue'>",
        "<a:Value>", statusCode.toString(), "</a:Value>",
        "</c:value>",
        "</a:KeyValuePairOfstringanyType>",
        "</a:Parameters>",
        "<a:RequestId i:nil='true' />",
        "<a:RequestName>SetState</a:RequestName>",
        "</request>"
      ].join("");

      return execute(request, async);
    };

    ///
    /// Associates the record (target) with a set of other records based on relationship
    /// (target should be either 'crmEntity' or 'crmEntityReference')
    /// (relatedBusinessEntities should be a list of either 'crmEntity' or 'crmEntityReference')
    ///

    var associate = function (relationshipName, target, relatedBusinessEntities, async) {
      var relatedEntities = relatedBusinessEntities;

      relatedEntities = isArray(relatedEntities) ? relatedEntities : [relatedEntities];

      var output = [];
      for (var i = 0; i < relatedEntities.length; i++) {
        if (relatedEntities[i].id != '') {
          output.push("<a:EntityReference>",
            "<a:Id>", relatedEntities[i].id, "</a:Id>",
            "<a:LogicalName>", relatedEntities[i].logicalName, "</a:LogicalName>",
            "<a:Name i:nil='true' />",
            "</a:EntityReference>");
        }
      }

      var relatedXml = output.join("");

      var request = [
        "<request i:type='a:AssociateRequest' xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts'>",
        "<a:Parameters xmlns:b='http://schemas.datacontract.org/2004/07/System.Collections.Generic'>",
        "<a:KeyValuePairOfstringanyType>",
        "<b:key>Target</b:key>",
        "<b:value i:type='a:EntityReference'>",
        "<a:Id>", target.id, "</a:Id>",
        "<a:LogicalName>", target.logicalName, "</a:LogicalName>",
        "<a:Name i:nil='true' />",
        "</b:value>",
        "</a:KeyValuePairOfstringanyType>",
        "<a:KeyValuePairOfstringanyType>",
        "<b:key>Relationship</b:key>",
        "<b:value i:type='a:Relationship'>",
        "<a:PrimaryEntityRole i:nil='true' />",
        "<a:SchemaName>", relationshipName, "</a:SchemaName>",
        "</b:value>",
        "</a:KeyValuePairOfstringanyType>",
        "<a:KeyValuePairOfstringanyType>",
        "<b:key>RelatedEntities</b:key>",
        "<b:value i:type='a:EntityReferenceCollection'>",
        relatedXml,
        "</b:value>",
        "</a:KeyValuePairOfstringanyType>",
        "</a:Parameters>",
        "<a:RequestId i:nil='true' />",
        "<a:RequestName>Associate</a:RequestName>",
        "</request>"
      ].join("");

      return execute(request, async);
    };

    ///
    /// Disassociates the record (target) with a set of other records based on relationship
    /// (target should be either 'crmEntity' or 'crmEntityReference')
    /// (relatedBusinessEntities should be a list of either 'crmEntity' or 'crmEntityReference')
    ///

    var disassociate = function (relationshipName, target, relatedBusinessEntities, async) {
      var relatedEntities = relatedBusinessEntities;

      relatedEntities = isArray(relatedEntities) ? relatedEntities : [relatedEntities];

      var output = [];
      for (var i = 0; i < relatedEntities.length; i++) {
        if (relatedEntities[i].id != '') {
          output.push("<a:EntityReference>",
            "<a:Id>", relatedEntities[i].id, "</a:Id>",
            "<a:LogicalName>", relatedEntities[i].logicalName, "</a:LogicalName>",
            "<a:Name i:nil='true' />",
            "</a:EntityReference>");
        }
      }

      var relatedXml = output.join("");

      var request = [
        "<request i:type='a:DisassociateRequest' xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts'>",
        "<a:Parameters xmlns:b='http://schemas.datacontract.org/2004/07/System.Collections.Generic'>",
        "<a:KeyValuePairOfstringanyType>",
        "<b:key>Target</b:key>",
        "<b:value i:type='a:EntityReference'>",
        "<a:Id>", target.id, "</a:Id>",
        "<a:LogicalName>", target.logicalName, "</a:LogicalName>",
        "<a:Name i:nil='true' />",
        "</b:value>",
        "</a:KeyValuePairOfstringanyType>",
        "<a:KeyValuePairOfstringanyType>",
        "<b:key>Relationship</b:key>",
        "<b:value i:type='a:Relationship'>",
        "<a:PrimaryEntityRole i:nil='true' />",
        "<a:SchemaName>", relationshipName, "</a:SchemaName>",
        "</b:value>",
        "</a:KeyValuePairOfstringanyType>",
        "<a:KeyValuePairOfstringanyType>",
        "<b:key>RelatedEntities</b:key>",
        "<b:value i:type='a:EntityReferenceCollection'>",
        relatedXml,
        "</b:value>",
        "</a:KeyValuePairOfstringanyType>",
        "</a:Parameters>",
        "<a:RequestId i:nil='true' />",
        "<a:RequestName>Disassociate</a:RequestName>",
        "</request>"
      ].join("");

      return execute(request, async);
    };

    ///
    /// Executes a fetchXml-request and enables paging
    ///

    function fetchMore(fetchXml, async) {

      // default is true
      async = (async === false) ? false : true;

      // defered object
      var dfd = $.Deferred(),
        request = ['<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">',
          ' <s:Body>',
          '  <Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services">',
          '     <request i:type="b:RetrieveMultipleRequest" xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" ',
          '         xmlns:i="http://www.w3.org/2001/XMLSchema-instance">',
          '             <b:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">',
          '             <b:KeyValuePairOfstringanyType>',
          '                 <c:key>Query</c:key>',
          '                 <c:value i:type="b:FetchExpression">',
          '                     <b:Query>',
          crmXmlEncode(fetchXml),
          '                     </b:Query>',
          '                 </c:value>',
          '             </b:KeyValuePairOfstringanyType>',
          '         </b:Parameters>',
          '         <b:RequestId i:nil="true"/>',
          '         <b:RequestName>RetrieveMultiple</b:RequestName>',
          '     </request>',
          ' </Execute>',
          '</s:Body></s:Envelope>'].join('');

      executeRequest(request, 'Execute', async).then(function (data, status, xhr) {

        dfd.resolve(getFetchResult(data, xhr));

      }, dfd.reject);

      // return the promise object
      return dfd.promise();
    }

    ///
    /// Aync-only: Loads all records (recursive with paging cookie)
    ///

    function fetchAll(fetchXml, pageSize, page, pagingCookie) {

      // defered object
      var dfd = $.Deferred(),
        allRecords = [];

      page = page || 1;

      // execute the fetch an receive the details (paging-cookie..)
      fetch(fetchXml, pageSize, page, pagingCookie, true).then(function (result) {

        // add the elements to the collection
        allRecords = allRecords.concat(result.entities);

        if (result.moreRecords) {
          page++;
          pagingCookie = result.pagingCookie;
          fetchAll(fetchXml, pageSize, page, pagingCookie).then(function (result2) {
            // add the items to the collection
            allRecords = allRecords.concat(result2.entities);
            result2.entities = allRecords;
            dfd.resolve(result2);

          }, dfd.reject);
        } else {
          result.entities = allRecords;
          dfd.resolve(result);
        }
      }, dfd.reject);

      return dfd.promise();
    }

    ///
    /// Executes a fetch-request and returns a promies object
    ///

    function fetch(fetchXml, pageSize, page, pagingCookie, async) {

      // default is true
      async = (async === false) ? false : true;

      // defered object
      var dfd = $.Deferred();

      var pagingFetchXml = injectPagingDetails(fetchXml, pageSize, page, pagingCookie);
      fetchMore(pagingFetchXml, async).then(function (result) {
        dfd.resolve(result);
      },
        dfd.reject);

      return dfd.promise();
    }

    function retrieveEntityMetadata(entityFilters, logicalName, retrieveAsIfPublished, async) {
      ///<summary>
      /// Sends an synchronous/asynchronous RetreiveEntityMetadata Request to retrieve a particular entity metadata in the system
      ///</summary>
      ///<returns>Entity Metadata</returns>
      ///<param name="entityFilters" type="String">
      /// The filter string available to filter which data is retrieved. Case Sensitive filters [Entity,Attributes,Privileges,Relationships]
      /// Include only those elements of the entity you want to retrieve in the array. Retrieving all parts of all entities may take significant time.
      ///</param>
      ///<param name="logicalName" type="String">
      /// The string of the entity logical name
      ///</param>
      ///<param name="retrieveAsIfPublished" type="Boolean">
      /// Sets whether to retrieve the metadata that has not been published.
      ///</param>
      ///<param name="async" type="Function">
      /// Indicator if the function is synchronous/asynchronous
      ///</param>

      retrieveAsIfPublished = (retrieveAsIfPublished === true) ? true : false;
      
      entityFilters = isArray(entityFilters) ? entityFilters : [entityFilters];
      var entityFiltersString = "";
      for (var iii = 0; iii < entityFilters.length; iii++) {
        entityFiltersString += encodeValue(entityFilters[iii]) + " ";
      }

      var request = [
          "<request i:type=\"a:RetrieveEntityRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">",
              "<a:Parameters xmlns:b=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">",
                  "<a:KeyValuePairOfstringanyType>",
                      "<b:key>EntityFilters</b:key>",
                      "<b:value i:type=\"c:EntityFilters\" xmlns:c=\"http://schemas.microsoft.com/xrm/2011/Metadata\">", encodeValue(entityFiltersString), "</b:value>",
                  "</a:KeyValuePairOfstringanyType>",
                  "<a:KeyValuePairOfstringanyType>",
                      "<b:key>MetadataId</b:key>",
                      "<b:value i:type=\"c:guid\"  xmlns:c=\"http://schemas.microsoft.com/2003/10/Serialization/\">", emptyGuid, "</b:value>",
                  "</a:KeyValuePairOfstringanyType>",
                  "<a:KeyValuePairOfstringanyType>",
                      "<b:key>RetrieveAsIfPublished</b:key>",
                      "<b:value i:type=\"c:boolean\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", retrieveAsIfPublished.toString(), "</b:value>",
                  "</a:KeyValuePairOfstringanyType>",
                  "<a:KeyValuePairOfstringanyType>",
                      "<b:key>LogicalName</b:key>",
                      "<b:value i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", logicalName, "</b:value>",
                  "</a:KeyValuePairOfstringanyType>",
              "</a:Parameters>",
              "<a:RequestId i:nil=\"true\" />",
              "<a:RequestName>RetrieveEntity</a:RequestName>",
           "</request>"].join("");

      var dfd = $.Deferred();

      execute(request, async).then(function (response) {
        var value = $(response).find(('b\\:value, value')).eq(0)[0];
        var result = objectifyMetadataNode(value);
        result._type = "EntityMetadata";
        dfd.resolve(result);
      },
        dfd.reject);

      return dfd.promise();
    };

    function retrieveRelationship(relationshipName, retrieveAsIfPublished, async) {
      ///<summary>
      /// Sends an synchronous/asynchronous RetrieveRelationshipMetadata Request to retrieve a particular entity's relationship metadata
      ///</summary>
      ///<returns>Relationship Metadata</returns>
      ///<param name="relationshipName" type="String">
      /// The string of the relathionship name
      ///</param>
      ///<param name="retrieveAsIfPublished" type="Boolean">
      /// Sets whether to retrieve the metadata that has not been published.
      ///</param>
      ///<param name="async" type="Function">
      /// Indicator if the function is synchronous/asynchronous
      ///</param>

      retrieveAsIfPublished = (retrieveAsIfPublished === true) ? true : false;
      var request = [
        "<request i:type=\"a:RetrieveRelationshipRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">",
        "  <a:Parameters xmlns:b=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">",
        "    <a:KeyValuePairOfstringanyType>",
        "      <b:key>MetadataId</b:key>",
        "      <b:value i:type=\"c:guid\" xmlns:c=\"http://schemas.microsoft.com/2003/10/Serialization/\">", emptyGuid, "</b:value>",
        "    </a:KeyValuePairOfstringanyType>",
        "    <a:KeyValuePairOfstringanyType>",
        "      <b:key>RetrieveAsIfPublished</b:key>",
        "      <b:value i:type=\"c:boolean\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", retrieveAsIfPublished.toString(), "</b:value>",
        "    </a:KeyValuePairOfstringanyType>",
        "    <a:KeyValuePairOfstringanyType>",
        "      <b:key>Name</b:key>",
        "      <b:value i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", relationshipName , "</b:value>",
        "    </a:KeyValuePairOfstringanyType>",
        "  </a:Parameters>",
        "  <a:RequestId i:nil=\"true\" />",
        "  <a:RequestName>RetrieveRelationship</a:RequestName>",
        "</request>"
        ].join("");

      var dfd = $.Deferred();

      execute(request, async).then(function (response) {
        var value = $(response).find(('b\\:value, value')).eq(0)[0];
        var result = objectifyMetadataNode(value);
        result._type = "RelationshipMetadata";
        dfd.resolve(result);
      },
        dfd.reject);

      return dfd.promise();
    }

    function retrieveAttributeMetadata (entityLogicalName, attributeLogicalName, retrieveAsIfPublished, async) {
      ///<summary>
      /// Sends an synchronous/asynchronous RetrieveAttributeMetadata Request to retrieve a particular entity's attribute metadata in the system
      ///</summary>
      ///<returns>Entity Metadata</returns>
      ///<param name="entityLogicalName" type="String">
      /// The string of the entity logical name
      ///</param>
      ///<param name="attributeLogicalName" type="String">
      /// The string of the entity's attribute logical name
      ///</param>
      ///<param name="retrieveAsIfPublished" type="Boolean">
      /// Sets whether to retrieve the metadata that has not been published.
      ///</param>
      ///<param name="async" type="Function">
      /// Indicator if the function is synchronous/asynchronous
      ///</param>

      var request = [
           "<request i:type=\"a:RetrieveAttributeRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">",
            "<a:Parameters xmlns:b=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">",
             "<a:KeyValuePairOfstringanyType>",
              "<b:key>EntityLogicalName</b:key>",
              "<b:value i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", entityLogicalName, "</b:value>",
             "</a:KeyValuePairOfstringanyType>",
             "<a:KeyValuePairOfstringanyType>",
              "<b:key>MetadataId</b:key>",
              "<b:value i:type=\"ser:guid\"  xmlns:ser=\"http://schemas.microsoft.com/2003/10/Serialization/\">", emptyGuid, "</b:value>",
             "</a:KeyValuePairOfstringanyType>",
              "<a:KeyValuePairOfstringanyType>",
              "<b:key>RetrieveAsIfPublished</b:key>",
            "<b:value i:type=\"c:boolean\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", retrieveAsIfPublished.toString(), "</b:value>",
             "</a:KeyValuePairOfstringanyType>",
             "<a:KeyValuePairOfstringanyType>",
              "<b:key>LogicalName</b:key>",
              "<b:value i:type=\"c:string\"   xmlns:c=\"http://www.w3.org/2001/XMLSchema\">", attributeLogicalName, "</b:value>",
             "</a:KeyValuePairOfstringanyType>",
            "</a:Parameters>",
            "<a:RequestId i:nil=\"true\" />",
            "<a:RequestName>RetrieveAttribute</a:RequestName>",
           "</request>"].join("");

      var dfd = $.Deferred();

      execute(request, async).then(function (response) {
        var value = $(response).find(('b\\:value, value')).eq(0)[0];
        var result = objectifyMetadataNode(value);
        result._type = "AttributeMetadata";
        dfd.resolve(result);
      },
        dfd.reject);

      return dfd.promise();
    };

    /// ===============================================
    /// CUSTOM CRM OBJECTS 
    /// ===============================================

    var crmValue = function (type, value, formattedValue) {
      this.type = type;
      this.value = value;
      this.formattedValue = formattedValue;
    };

    crmValue.prototype.getKeyValue = function () {
      return this.value;
    };

    var crmEntityReference = function (id, logicalName, name) {
      this.id = id;
      this.logicalName = logicalName;
      this.name = name;
      this.type = 'EntityReference';
    };

    crmEntityReference.prototype.getKeyValue = function () {
      return this.id;
    };

    var crmEntityCollection = function (items) {
      this.value = items;
      this.type = 'EntityCollection';
    };

    crmEntityCollection.prototype.getKeyValue = function () {
      var items = this.value;
      if (!items) {
        return null;
      }

      var output = [];
      for (var i = 0, max = items.length; i < max; i++) {
        output.push(items[i].id);
      }

      return output;
    };

    var crmOptionSetValue = function (value, formattedValue) {
      this.value = value;
      this.formattedValue = formattedValue;
      this.type = 'OptionSetValue';
    };

    crmOptionSetValue.prototype.getKeyValue = function () {
      return this.value;
    };

    var crmEntity = function (logicalName, id) {
      this.id = (!id) ? emptyGuid : id;
      this.logicalName = logicalName;
      this.attributes = {};
    };

    crmEntity.prototype = {
      // Serialize a CRM Business Entity object to XML string in order to be passed to CRM Web Services.
      //  @return {String} The serialized XML string of CRM entity.
      serialize: function () {
        var xml = ['<entity xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">'];
        xml.push('<a:Attributes xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">');

        for (var attributeName in this.attributes) {
          var attribute = this.attributes[attributeName];

          xml.push('<a:KeyValuePairOfstringanyType>');
          xml.push('<b:key>', attributeName, '</b:key>');

          if (attribute === null || attribute.value === null) {
            xml.push('<b:value i:nil="true" />');
          } else {
            var type = (!attribute.type)
              ? typeof attribute
              : crmXmlEncode(attribute.type);
            var value;
            var encodedValue;
            var id;
            var encodedId;
            var logicalName;
            var encodedLogicalName;
            switch (type) {
              case "OptionSetValue":
                value = (attribute.hasOwnProperty("value")) ? attribute["value"] : attribute;
                encodedValue = encodeValue(value);
                xml.push('<b:value i:type="a:OptionSetValue">');
                xml.push('<a:Value>', encodedValue, '</a:Value>', '</b:value>');
                break;
              case "EntityCollection":
                xml.push('<b:value i:type="a:EntityCollection">');
                xml.push('<a:Entities>');
                value = (attribute.hasOwnProperty("value")) ? attribute["value"] : attribute;
                var collections = isArray(value) ? value : [value];

                for (var i = 0; i < collections.length; i++) {
                  var item = collections[i];
                  id = (item.hasOwnProperty("id")) ? item["id"] : item;
                  encodedId = encodeValue(id);
                  logicalName = (item.hasOwnProperty("logicalName")) ? item["logicalName"] : item;
                  encodedLogicalName = encodeValue(logicalName);
                  xml.push('<a:Entity>');
                  xml.push('<a:Attributes>');
                  xml.push('<a:KeyValuePairOfstringanyType>');
                  xml.push('<b:key>partyid</b:key>');
                  xml.push('<b:value i:type="a:EntityReference">');
                  xml.push('<a:Id>', encodedId, '</a:Id>');
                  xml.push('<a:LogicalName>', encodedLogicalName, '</a:LogicalName>');
                  xml.push('<a:Name i:nil="true" />');
                  xml.push('</b:value>');
                  xml.push('</a:KeyValuePairOfstringanyType>');
                  xml.push('</a:Attributes>');
                  xml.push('<a:EntityState i:nil="true" />');
                  xml.push('<a:FormattedValues />');
                  xml.push('<a:Id>00000000-0000-0000-0000-000000000000</a:Id>');
                  xml.push('<a:LogicalName>activityparty</a:LogicalName>');
                  xml.push('<a:RelatedEntities />');
                  xml.push('</a:Entity>');
                }
                xml.push('</a:Entities>');
                xml.push('<a:EntityName i:nil="true" />');
                xml.push('<a:MinActiveRowVersion i:nil="true" />');
                xml.push('<a:MoreRecords>false</a:MoreRecords>');
                xml.push('<a:PagingCookie i:nil="true" />');
                xml.push('<a:TotalRecordCount>0</a:TotalRecordCount>');
                xml.push('<a:TotalRecordCountLimitExceeded>false</a:TotalRecordCountLimitExceeded>');
                xml.push('</b:value>');
                break;
              case "EntityReference":
                id = (attribute.hasOwnProperty("id")) ? attribute["id"] : attribute;
                encodedId = encodeValue(id);
                logicalName = (attribute.hasOwnProperty("logicalName")) ? attribute["logicalName"] : attribute;
                encodedLogicalName = encodeValue(logicalName);
                xml.push('<b:value i:type="a:EntityReference">');
                xml.push('<a:Id>', encodedId, '</a:Id>');
                xml.push('<a:LogicalName>', encodedLogicalName, '</a:LogicalName>');
                xml.push('<a:Name i:nil="true" />', '</b:value>');
                break;
              case "Money":
                value = (attribute.hasOwnProperty("value")) ? attribute["value"] : attribute;
                encodedValue = encodeValue(value);
                xml.push('<b:value i:type="a:Money">');
                xml.push('<a:Value>', encodedValue, '</a:Value>', '</b:value>');
                break;
              case "guid":
                value = (attribute.hasOwnProperty("value")) ? attribute["value"] : attribute;
                encodedValue = encodeValue(value);
                xml.push('<b:value i:type="c:guid" xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/">');
                xml.push(encodedValue, '</b:value>');
                break;
              case "number":
                value = (attribute.hasOwnProperty("value")) ? attribute["value"] : attribute;
                encodedValue = encodeValue(value);
                var oType = (parseInt(encodedValue) == encodedValue) ? "c:int" : "c:decimal";
                xml.push('<b:value i:type="', oType, '" xmlns:c="http://www.w3.org/2001/XMLSchema">');
                xml.push(encodedValue, '</b:value>');
                break;
              default:
                value = (attribute.hasOwnProperty("value")) ? attribute["value"] : attribute;
                encodedValue = encodeValue(value);
                type = (typeof value === "object" && value.getTime) ? "dateTime" : type;
                xml.push('<b:value i:type="c:', type, '" xmlns:c="http://www.w3.org/2001/XMLSchema">', encodedValue, '</b:value>');
                break;
            }
          }

          xml.push('</a:KeyValuePairOfstringanyType>');
        }

        xml.push('</a:Attributes><a:EntityState i:nil="true" />');
        xml.push('<a:FormattedValues xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />');
        xml.push('<a:Id>', encodeValue(this.id), '</a:Id>');
        xml.push('<a:LogicalName>', this.logicalName, '</a:LogicalName>');
        xml.push('<a:RelatedEntities xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />');
        xml.push('</entity>');
        return xml.join("");
      },

      toEntityReference: function (name) {
        return new crmEntityReference(this.id, this.logicalName, name);
      },

      getKeyValue: function (attribute) {
        var o = this.attributes[attribute];
        if (!o) {
          return undefined;
        }

        return o.getKeyValue();
      }
    };

    var fetchElem = function (entityName, distinct, aggregate, attributeElems, orderElems, filterElem, linkedEntityElems) {
      this.entityName = entityName;
      this.distinct = distinct || false;
      this.aggregate = aggregate || false;
      this.attributeElems = attributeElems || [];
      this.orderElems = orderElems || [];
      this.filterElem = filterElem || [];
      this.linkedEntityElems = linkedEntityElems || [];
    };

    fetchElem.prototype.toString = function () {
      return [
        "<fetch distinct='" + this.distinct + "' aggregate='" + this.aggregate + "' mapping='logical' output-format='xml-platform'>",
        "  <entity name='", this.entityName, "'>",
        buildFetchSetString(this.attributeElems),
        buildFetchSetString(this.orderElems),
        this.filterElem.toString(),
        buildFetchSetString(this.linkedEntityElems),
        "  </entity>",
        "</fetch>"
      ].join("");

    };

    var fetchAttributeElem = function (name, aggregate, alias) {
      this.name = name;
      this.aggregate = aggregate;
      this.alias = alias;
    };

    fetchAttributeElem.prototype.toString = function () {
      var output = [];
      var name = this.name;
      if (!!name) {
        output.push("<attribute name='", name, "'");
        var aggregate = this.aggregate;
        if (!!aggregate) {
          output.push(" aggregate='", this.aggregate, "'");
        }

        var alias = this.alias;
        if (!!alias) {
          output.push(" alias='", alias, "'");
        }

        output.push("/>");
      }

      return output.join('');
    };

    var buildFetchAttributeElemSet = function (attributeNames) {
      var output = [];
      for (var i = 0, max = attributeNames.length; i < max; i++) {
        output.push(new fetchAttributeElem(attributeNames[i]));
      }

      return output;
    };

    var fetchOrderElem = function (attribute, descending) {
      this.attribute = attribute;
      this.descending = descending || false;
    };

    fetchOrderElem.prototype.toString = function () {
      var output = [];
      var attribute = this.attribute;
      if (!!attribute) {
        output.push("<order attribute='", attribute, "'");
        output.push(" descending='", this.descending, "'");
        output.push("/>");
      }

      return output.join('');
    };

    var fetchFilterElem = function (type, filters, conditions) {
      this.type = type || 'and';
      this.filters = filters || [];
      this.conditions = conditions || [];
    };

    fetchFilterElem.prototype.toString = function () {
      var output = [];
      output.push("<filter type='", this.type, "'>");
      output.push(buildFetchSetString(this.filters));
      output.push(buildFetchSetString(this.conditions));
      output.push("</filter>");
      return output.join('');
    };

    var fetchConditionElem = function (attribute, operator, value) {
      this.attribute = attribute;
      this.operator = operator;
      this.value = value;
    };

    fetchConditionElem.prototype.toString = function () {
      var output = [];
      output.push("<condition attribute='", this.attribute, "' operator='", this.operator, "' ");
      var value = this.value;
      if (value != "undefined" && value != null) {
        if (isArray(value)) {
          output.push('>');
          for (var i = 0, max = value.length; i < max; i++) {
            output.push("<value>", value[i], "</value>");
          }

          output.push('</condition>');
        } else {
          output.push(" value='", value, "'/>");
        }
      } else {
        output.push("/>");
      }

      return output.join('');
    };

    var fetchLinkedEntityElem = function (name, alias, from, to, linkType, visible, intersect, attributeElems, filterElem) {
      this.name = name;
      this.alias = alias;
      this.from = from;
      this.to = to;
      this.linkType = linkType || "outer";
      this.visible = visible || false;
      this.intersect = intersect || false;
      this.attributeElems = attributeElems || [];
      this.filterElem = filterElem || [];
    };

    fetchLinkedEntityElem.prototype.toString = function () {
      var output = [];
      output.push(
        "<link-entity name='", this.name, "' alias='", this.alias, "' from='", this.from, "' to='", this.to,
        "' link-type='", this.linkType, "' intersect='", this.intersect, "' visible='", this.visible, "'>");
      output.push(buildFetchSetString(this.attributeElems));
      output.push(this.filterElem.toString());
      output.push("</link-entity>");
      return output.join('');
    };


    /// ===============================================
    /// Webresources Helpers
    /// ===============================================

    function readXmlWebResource(path) {
      var dfd = $.Deferred();
      $.ajax({
        type: "GET",
        url: path,
        dataType: "xml"
      }).then(function (result) {
        dfd.resolve(result);
      },
        function (jqXhr, textStatus, errorThrown) {
          var errMessage = getResponseXmlErrorMessage(jqXhr);
          if (errMessage != null) {
            textStatus = 'errorRecognized';
            errorThrown = errMessage;
          }

          var error = { response: jqXhr, status: textStatus, message: errorThrown };
          dfd.reject(error);
        });

      return dfd.promise();
    }

    /// ===============================================
    /// OTHER HELPER METHODS 
    /// ===============================================

    ///
    /// Converst the xml definiton into an attribute object. 
    /// The joined attributes are evaluated via a recursive call of this function
    ///

    function buildFetchSetString(items) {
      var output = [];
      if (!!items) {
        if (typeof items == "string") {
          output.push(items);
        } else {
          for (var j = 0, m = items.length; j < m; j++) {
            output.push(items[j].toString());
          }
        }
      }

      return output.join('');
    }

    function convertXmlToAttributeObject(type, xmlNode) {

      var attr;

      switch (type) {
        case "a:OptionSetValue":
          attr = new crmOptionSetValue();
          attr.value = getNodeText(xmlNode);
          break;
        case "a:EntityReference":
          attr = new crmEntityReference();
          attr.id = getChildNodeText(xmlNode, 'a:Id');
          attr.name = getChildNodeText(xmlNode, 'a:Name');
          attr.logicalName = getChildNodeText(xmlNode, 'a:LogicalName');
          break;
        case "a:Money":
          attr = new crmValue();
          attr.type = type.replace('a:', '');
          attr.value = parseFloat(getNodeText(xmlNode), 10);
          break;
        case "a:AliasedValue":
          var aliasValue = getChildNode(xmlNode, 'a:Value'),
            aliasType = getAttribute(aliasValue, 'i:type');

          attr = convertXmlToAttributeObject(aliasType, aliasValue);
          break;
        case "a:EntityCollection":
          var items = [];
          var entityCollection = getChildNode(xmlNode, 'a:Entities');
          var entities = entityCollection.childNodes;
          for (var i = 0, max = entities.length; i < max; i++) {
            var attributes = getChildNode(entities[i], "a:Attributes").childNodes;
            for (var j = 0, max2 = attributes.length; j < max2; j++) {
              var a = attributes[j].childNodes[0];
              if (getNodeText(a) == 'partyid') {
                var v = attributes[j].childNodes[1];
                var ref = new crmEntityReference();
                ref.id = getChildNodeText(v, 'a:Id');
                ref.name = getChildNodeText(v, 'a:Name');
                ref.logicalName = getChildNodeText(v, 'a:LogicalName');
                items[i] = ref;
              }
            }
          }

          attr = new crmEntityCollection(items);

          break;
        case 'c:int':
          attr = new crmValue();
          attr.type = type.replace('c:', '').replace('a:', '');
          attr.value = parseInt(getNodeText(xmlNode), 10);
          break;
        case 'c:decimal':
        case 'c:double':
          attr = new crmValue();
          attr.type = type.replace('c:', '').replace('a:', '');
          attr.value = parseFloat(getNodeText(xmlNode));
          break;
        case 'c:dateTime':
          attr = new crmValue();
          attr.type = type.replace('c:', '').replace('a:', '');
          attr.value = parseIso8601Date(getNodeText(xmlNode));
          break;
        case 'c:boolean':
          attr = new crmValue();
          attr.type = type.replace('c:', '').replace('a:', '');
          attr.value = (getNodeText(xmlNode) !== 'true') ? false : true;
          break;
        default:
          attr = new crmValue();
          attr.type = type.replace('c:', '').replace('a:', '');
          attr.value = getNodeText(xmlNode);
          break;
      }

      return attr;
    }

    ///
    /// Parses "Attribute" nodes of the SOAP-response
    ///

    function parseAttibutes(entityNode) {

      var attributesNode = getChildNode(entityNode, 'a:Attributes');
      var typedAttrSet = {},
        attrNode = null,
        key = null,
        type = null,
        value = null;

      for (var i = 0, max = attributesNode.childNodes.length; i < max; i++) {

        attrNode = attributesNode.childNodes[i];

        // Establish the key for the attribute 
        key = getChildNodeText(attrNode, 'b:key');
        value = getChildNode(attrNode, 'b:value');
        type = getAttribute(value, 'i:type');

        // populate the object
        typedAttrSet[key] = convertXmlToAttributeObject(type, value);
      }

      return typedAttrSet;
    }

    ///
    /// Parses a single xml-node -> transforms into CrmEntity
    ///

    function parseSingleEntityNode(entityNode) {

      var entity = new crmEntity();

      entity.id = getChildNodeText(entityNode, 'a:Id');
      entity.logicalName = getChildNodeText(entityNode, 'a:LogicalName');

      entity.attributes = parseAttibutes(entityNode);

      // parse the formated values
      var childSet = getChildNode(entityNode, 'a:FormattedValues').childNodes;

      for (var i = 0, max = childSet.length; i < max; i++) {
        var item = childSet[i],
          key = getChildNodeText(item, 'b:key'),
          value = getChildNodeText(item, 'b:value');

        entity.attributes[key].formattedValue = value;
      }

      return entity;
    }

    ///
    /// Converts a given XMLDocument of XmlELement into a string
    ///

    function xmlToString(elem) {

      var serialized;

      if (window.XMLSerializer) {

        // XMLSerializer exists in current Mozilla browsers
        var serializer = new XMLSerializer();
        serialized = serializer.serializeToString(elem);
      } else {
        // Internet Explorer has a different approach to serializing XML
        serialized = elem.xml;
      }

      return serialized;
    }

    ///
    /// retrievs the text-value of the expression
    ///

    function getChildNodeText(xml, xpathExpression) {

      return getNodeText(getChildNode(xml, xpathExpression));
    }

    ///
    ///  Get a single child node that matches the specified name.
    /// 

    function getChildNode(xmlNode, nodeName) {

      for (var i = 0; i < xmlNode.childNodes.length; i++) {

        var childNode = xmlNode.childNodes[i];

        if (childNode.nodeName == nodeName) {
          return childNode;
        }
      }

      return null;
    }

    ///
    /// Get the attribute regardless of the namespace
    ///

    function getAttribute(xmlNode, attrName) {

      for (var i = 0; i < xmlNode.attributes.length; i++) {

        var attr = xmlNode.attributes[i];

        if (attr.name == attrName) {
          return attr.value;
        }
      }

      return null;
    }

    ///
    /// IE 9/10 and Chrome, Firefox, ... using "textContent" and IE 8 using "text
    ///

    function getNodeText(node) {

      return node.text !== undefined
        ? node.text
        : node.textContent;
    }

    function getResponseXmlErrorMessage(response) {
      var responseXml = response.responseXML;
      if (!responseXml) {
        return null;
      } else {
        var faultNode = responseXml.firstChild.firstChild.firstChild;
        return getChildNodeText(faultNode, 'faultstring');
      }
    }

    ///
    /// Converts the response to a result-object that contains the entities, pagaingcookie...
    ///

    function getFetchResult(data, xhr) {

      // "s:Envelope/s:Body/ExecuteResponse/ExecuteResult"
      var executeResult = data.firstChild.firstChild.firstChild.firstChild,
        resultsNode = getChildNode(executeResult, 'a:Results'),
        entityCollection = getChildNode(resultsNode.firstChild, 'b:value'),
        resultSet = getChildNode(entityCollection, 'a:Entities').childNodes;

      return {
        entityName: getChildNodeText(entityCollection, 'a:EntityName'),
        moreRecords: (getChildNodeText(entityCollection, 'a:MoreRecords') === 'true'),
        pagingCookie: getChildNodeText(entityCollection, 'a:PagingCookie'),
        totalRecordCount: parseInt(getChildNodeText(entityCollection, 'a:TotalRecordCount'), 10),
        entities: $.map(resultSet, parseSingleEntityNode)
      };
    }

    ///
    /// Injects the paging-cookie, page-count and record count
    ///

    function injectPagingDetails(fetchXml, pageSize, page, pagingCookie) {
      var xmldoc = $.parseXML(fetchXml),
        fetchElement = $(xmldoc).find('fetch');

      pageSize = pageSize || 5000;
      fetchElement.attr('count', pageSize);

      if (page) {
        fetchElement.attr('page', page);
      }

      if (pagingCookie) {
        fetchElement.attr('paging-cookie', pagingCookie);
      }

      return xmlToString(xmldoc);
    }

    ///
    /// Parses a date-string in ISO-format into a date-object
    ///

    function parseIso8601Date(s) {

      // parenthese matches:
      // year month day    hours minutes seconds  
      // dotmilliseconds 
      // tzstring plusminus hours minutes
      var re = /(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(\.\d+)?(Z|([+-])(\d\d):(\d\d))/;

      var d = [];
      d = s.match(re);

      if (!d) {
        throw "Couldn't parse ISO 8601 date string '" + s + "'";
      }

      // parse strings, leading zeros into proper ints
      var a = [1, 2, 3, 4, 5, 6, 10, 11];

      for (var i = 0, max = a.length; i < max; i++) {
        d[a[i]] = parseInt(d[a[i]], 10);
      }
      d[7] = parseFloat(d[7]);

      // Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
      // note that month is 0-11, not 1-12
      // see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/UTC
      var ms = Date.UTC(d[1], d[2] - 1, d[3], d[4], d[5], d[6]);

      // if there are milliseconds, add them
      if (d[7] > 0) {
        ms += Math.round(d[7] * 1000);
      }

      // if there's a timezone, calculate it
      if (d[8] !== 'Z' && d[10]) {

        var offset = d[10] * 60 * 60 * 1000;

        if (d[11]) {
          offset += d[11] * 60 * 1000;
        }
        if (d[9] === '-') {
          ms -= offset;
        } else {
          ms += offset;
        }
      }

      return new Date(ms);
    }

    function padNumber(s, len) {
      len = len || 2;

      s = '' + s;
      while (s.length < len) {
        s = "0" + s;
      }
      return s;
    }
    
    function encodeDate(dateTime) {
      return dateTime.getFullYear() + "-" +
        padNumber(dateTime.getMonth() + 1) + "-" +
        padNumber(dateTime.getDate()) + "T" +
        padNumber(dateTime.getHours()) + ":" +
        padNumber(dateTime.getMinutes()) + ":" +
        padNumber(dateTime.getSeconds());
    }
    
    function encodeValue(value) {
      return (typeof value === "object" && value.getTime)
        ? encodeDate(value)
        : crmXmlEncode(value);
    }
    
    function crmXmlDecode(s) {

      if (typeof window.CrmEncodeDecode != 'undefined') {
        return window.CrmEncodeDecode.CrmXmlDecode(s);
      }

      if (typeof s != "string") {
        s = s.toString();
      }

      return s;
    }
    
    function crmXmlEncode(s) {

      if (typeof window.CrmEncodeDecode != 'undefined') {
        return window.CrmEncodeDecode.CrmXmlEncode(s);
      }

      if ("undefined" == typeof s || "unknown" == typeof s || null == s) {
        return s;
      } else if (typeof s != "string") {
        s = s.toString();
      }
      
      return innerSurrogateAmpersandWorkaround(s);
    }

    function htmlEncode(s) {
      if (s == null || s == "") return s;
      for (var count = 0, buffer = "", hEncode = "", cnt = 0; cnt < s.length; cnt++) {
        var c = s.charCodeAt(cnt);
        if (c > 96 && c < 123 || c > 64 && c < 91 || c == 32 || c > 47 && c < 58 || c == 46 || c == 44 || c == 45 || c == 95)
          buffer += String.fromCharCode(c);
        else buffer += "&#" + c + ";";
        if (++count == 500) {
          hEncode += buffer;
          buffer = "";
          count = 0;
        }
      }
      if (buffer.length) hEncode += buffer;
      return hEncode;
    }
    
    function innerSurrogateAmpersandWorkaround(s) {
      var buffer = '';
      var c0;
      for (var cnt = 0; cnt < s.length; cnt++) {
        c0 = s.charCodeAt(cnt);
        if (c0 >= 55296 && c0 <= 57343)
          if (cnt + 1 < s.length) {
            var c1 = s.charCodeAt(cnt + 1);
            if (c1 >= 56320 && c1 <= 57343) {
              buffer += "CRMEntityReferenceOpen" + ((c0 - 55296) * 1024 + (c1 & 1023) + 65536).toString(16) + "CRMEntityReferenceClose";
              cnt++;
            } else
              buffer += String.fromCharCode(c0);
          } else buffer += String.fromCharCode(c0);
        else buffer += String.fromCharCode(c0);
      }
      s = buffer;
      buffer = "";
      for (cnt = 0; cnt < s.length; cnt++) {
        c0 = s.charCodeAt(cnt);
        if (c0 >= 55296 && c0 <= 57343)
          buffer += String.fromCharCode(65533);
        else buffer += String.fromCharCode(c0);
      }
      s = buffer;
      s = htmlEncode(s);
      s = s.replace(/CRMEntityReferenceOpen/g, "&#x");
      s = s.replace(/CRMEntityReferenceClose/g, ";");
      return s;
    }
    
    function isArray(input) {
      return input.constructor.toString().indexOf("Array") != -1;
    }

    // Inspired From Microsoft SDK code to retrieve Metadata using JavaScript
    // Copyright (C) Microsoft Corporation.  All rights reserved.
    var metadataArrayElements = ["Attributes",
                               "ManyToManyRelationships",
                               "ManyToOneRelationships",
                               "OneToManyRelationships",
                               "Privileges",
                               "LocalizedLabels",
                               "Options",
                               "Targets"];

    var isMetadataArray = function (elementName) {
      for (var i = 0; i < metadataArrayElements.length; i++) {
        if (elementName === metadataArrayElements[i]) {
          return true;
        }
      }
      return false;
    };

    var getMetadataNodeName = function (node) {
      if (typeof (node.baseName) != "undefined") {
        return node.baseName;
      }
      else {
        return node.localName;
      }
    };

    var objectifyMetadataNode = function (node) {
      //Check for null
      if (node.attributes != null && node.attributes.length == 1) {
        if (node.attributes.getNamedItem("i:nil") != null && node.attributes.getNamedItem("i:nil").nodeValue == "true") {
          return null;
        }
      }

      //Check if it is a value
      if ((node.firstChild != null) && (node.firstChild.nodeType == 3)) {
        var nodeName = getMetadataNodeName(node);

        switch (nodeName) {
          //Integer Values        
          case "ActivityTypeMask":
          case "ObjectTypeCode":
          case "ColumnNumber":
          case "DefaultFormValue":
          case "MaxValue":
          case "MinValue":
          case "MaxLength":
          case "Order":
          case "Precision":
          case "PrecisionSource":
          case "LanguageCode":
            return parseInt(node.firstChild.nodeValue, 10);
            
            // Boolean values
          case "AutoRouteToOwnerQueue":
          case "CanBeChanged":
          case "CanTriggerWorkflow":
          case "IsActivity":
          case "IsActivityParty":
          case "IsAvailableOffline":
          case "IsChildEntity":
          case "IsCustomEntity":
          case "IsCustomOptionSet":
          case "IsDocumentManagementEnabled":
          case "IsEnabledForCharts":
          case "IsGlobal":
          case "IsImportable":
          case "IsIntersect":
          case "IsManaged":
          case "IsReadingPaneEnabled":
          case "IsValidForAdvancedFind":
          case "CanBeSecuredForCreate":
          case "CanBeSecuredForRead":
          case "CanBeSecuredForUpdate":
          case "IsCustomAttribute":
          case "IsPrimaryId":
          case "IsPrimaryName":
          case "IsSecured":
          case "IsValidForCreate":
          case "IsValidForRead":
          case "IsValidForUpdate":
          case "IsCustomRelationship":
          case "CanBeBasic":
          case "CanBeDeep":
          case "CanBeGlobal":
          case "CanBeLocal":
            return (node.firstChild.nodeValue === "true") ? true : false;
            //OptionMetadata.Value and BooleanManagedProperty.Value and AttributeRequiredLevelManagedProperty.Value
            
          case "Value":
            //BooleanManagedProperty.Value
            if ((node.firstChild.nodeValue === "true") || (node.firstChild.nodeValue == "false")) {
              return (node.firstChild.nodeValue == "true") ? true : false;
            }
            
            //AttributeRequiredLevelManagedProperty.Value
            if (
                  (node.firstChild.nodeValue == "ApplicationRequired") ||
                  (node.firstChild.nodeValue == "None") ||
                  (node.firstChild.nodeValue == "Recommended") ||
                  (node.firstChild.nodeValue == "SystemRequired")
               ) {
              return node.firstChild.nodeValue;
            }

            //OptionMetadata.Value
            return parseInt(node.firstChild.nodeValue, 10);
            
            //String values        
          default:
            return node.firstChild.nodeValue;
        }
      }

      //Check if it is a known array
      if (isMetadataArray(getMetadataNodeName(node))) {
        var arrayValue = [];
        for (var iii = 0; iii < node.childNodes.length; iii++) {
          var objectTypeName;
          if ((node.childNodes[iii].attributes != null) && (node.childNodes[iii].attributes.getNamedItem("i:type") != null)) {
            objectTypeName = node.childNodes[iii].attributes.getNamedItem("i:type").nodeValue.split(":")[1];
          }
          else {

            objectTypeName = getMetadataNodeName(node.childNodes[iii]);
          }

          var b = objectifyMetadataNode(node.childNodes[iii]);
          b._type = objectTypeName;
          arrayValue.push(b);

        }

        return arrayValue;
      }

      //Null entity description labels are returned as <label/> - not using i:nil = true;
      if (node.childNodes.length == 0) {
        return null;
      }

      //Otherwise return an object
      var c = {};
      if (node.attributes.getNamedItem("i:type") != null) {
        c._type = node.attributes.getNamedItem("i:type").nodeValue.split(":")[1];
      }
      for (var i = 0; i < node.childNodes.length; i++) {
        if (node.childNodes[i].nodeType == 3) {
          c[getMetadataNodeName(node.childNodes[i])] = node.childNodes[i].nodeValue;
        }
        else {
          c[getMetadataNodeName(node.childNodes[i])] = objectifyMetadataNode(node.childNodes[i]);
        }

      }
      return c;
    };

    /// ===============================================
    /// Public API
    /// ===============================================
    return {
      CrmEntity: crmEntity,
      CrmEntityReference: crmEntityReference,
      CrmEntityCollection: crmEntityCollection,
      CrmOptionSetValue: crmOptionSetValue,
      CrmValue: crmValue,

      Create: createEntity,
      Update: updateEntity,
      Delete: deleteEntity,
      Assign: assignEntity,
      SetState: setState,
      Associate: associate,
      Disassociate: disassociate,
      Execute: execute,

      Retrieve: retrieveEntity,
      Fetch: fetch,
      FetchAll: fetchAll,

      RetrieveEntityMetadata: retrieveEntityMetadata,
      RetrieveRelationship: retrieveRelationship,
      RetrieveAttributeMetadata: retrieveAttributeMetadata,

      FetchElem: fetchElem,
      FetchAttributeElem: fetchAttributeElem,
      FetchOrderElem: fetchOrderElem,
      FetchFilterElem: fetchFilterElem,
      FetchConditionElem: fetchConditionElem,
      FetchLinkedEntityElem: fetchLinkedEntityElem,

      BuildFetchAttributeElemSet: buildFetchAttributeElemSet,
      
      XmlToString: xmlToString,
      ReadXmlWebResource: readXmlWebResource,
      
      GetServerUrl: getServerUrl
    };
  }();
})(window.crmjQuery, window.crmjQuery);